﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Transactions;
using System.Xml;

using LostLib.Collections.Generic;
using LostLib.Programs.Properties;


namespace LostLib.Programs
{	
	/// <summary>
	/// This class exposes some methods to access programs database, install and remove packages e.t.c.
	/// </summary>
	public static class PackageManager
	{
		#region Database
		/// <summary>
		/// Creates local database for LostLib.Programs.
		/// </summary>
		public static void CreateDatabase()
		{
			if (appDB.DatabaseExists())
				throw new InvalidOperationException("LostLib.Programs is installed.\nYou must remove previous installation first.");

			CliPackageManager.WriteDebug("creating new database...");
			if (appDB != null)
			{
				appDB.Dispose();
				appDB = new ApplicationsDatabaseDataContext(Settings.Default.ProgramsDB);
			}

			appDB.CreateDatabase();

			var source = new PackageSource() {
				Name = "built-in",
				Uri = Settings.Default.BuiltInSource,
				Version = DateTime.Now,
			};
			appDB.PackageSources.InsertOnSubmit(source);
			appDB.SubmitChanges();
			CliPackageManager.WriteDebug("OK\n");

			UpdateSource("built-in");

			CliPackageManager.WriteDebug("creating {0}...", Settings.Default.PackageCache);
			Directory.CreateDirectory(Settings.Default.PackageCache);
			CliPackageManager.WriteDebug("OK\n");
		}

		/// <summary>
		/// Determines whether local database is installed.
		/// </summary>
		public static bool Installed
		{
			get
			{
				return appDB.DatabaseExists();
			}
		}

		static ApplicationsDatabaseDataContext appDB = new ApplicationsDatabaseDataContext(Settings.Default.ProgramsDB);

		/// <summary>
		/// Gets local LostLib.Programs database.
		/// </summary>
		public static ApplicationsDatabaseDataContext ApplicationDatabase
		{ 
			get 
			{
				if (!Installed) throw new InvalidOperationException("LostLib.Programs is not installed");
				return appDB;
			}
		}
		#endregion Database

		#region Source updating
		static void UpdatePackageVersions(PackageSource source, Package package, XmlNode packageNode)
		{
			foreach (XmlNode versionNode in packageNode.SelectNodes("version"))
			{
				var version = PackageVersion.Parse(versionNode);
				version.PackageName = package.PackageName;
				version.SourceID = source.Name;

				var current = package.PackageVersions.Where(ver => ver.VersionString == version.VersionString).FirstOrDefault();
				version = current ?? version;

				version.SourceID = source.Name;

				if (current != null) continue;

				appDB.PackageVersions.InsertOnSubmit(version);

				foreach (XmlNode dependancyNode in versionNode.SelectNodes("dependancy"))
				{
					var dependancy = VersionDependancy.Parse(dependancyNode);
					dependancy.VersionString = version.VersionString;

					appDB.VersionDependancies.InsertOnSubmit(dependancy);
				}
			}
		}
		internal static void UpdateSourceInternal(string sourceName)
		{
			//TODO: rewrite via LINQ to XML
			CliPackageManager.WriteDebug("updating source {0}...", sourceName);
			var source = appDB.PackageSources.Where(src => src.Name == sourceName).FirstOrDefault();

			if (source == null)
				throw new KeyNotFoundException(sourceName);

			var xml = new XmlDocument();
			xml.Load(source.Uri);
			foreach (var version in source.PackageVersions)
				version.SourceID = null;

			foreach (XmlNode packageNode in xml.SelectNodes("/PackagesDB/package"))
			{
				var package = Package.Parse(packageNode);

				var current = appDB.Packages.Where(pkg => pkg.PackageName == package.PackageName).FirstOrDefault();
				package = current ?? package;

				if (current == null)
					appDB.Packages.InsertOnSubmit(package);

				UpdatePackageVersions(source, package, packageNode);
			}

			appDB.PackageVersions.DeleteAllOnSubmit(appDB.PackageVersions.Where(ver => ver.SourceID == null));
			RemoveEmptyPackages();

			source.Version = DateTime.Now;
			appDB.SubmitChanges();
			CliPackageManager.WriteDebug("OK\n");
		}
		/// <summary>
		/// Downloads latest package list for specified source.
		/// </summary>
		/// <param name="sourceName">Name of source to update.</param>
		/// <remarks>This function has special handling for built-in source.</remarks>
		public static void UpdateSource(string sourceName)
		{
			UpdateSourceInternal(sourceName);
		}
		#endregion Source updating

		#region Remove functions
		//TODO: clean garbage
		static void BuildRemoveList(IEnumerable<PackageVersion> removeList, List<PackageVersion> removeOrder, HashSet<PackageVersion> removed)
		{
			if (removeOrder== null) throw new ArgumentNullException("removed");

			foreach (var version in removeList)
			{
				if (removed.Contains(version)) continue;

				removeOrder.Add(version);
				removed.Add(version);

				var toRemove =
					appDB.PackageVersions.Where(versionToRemove =>
						versionToRemove.Installed &&
						(versionToRemove.VersionDependancies.Count(dependency => 
							(dependency.DependancyType == "newer")
							&& (dependency.PackageName == version.PackageName)) > 0)).ToArray();


				Func<PackageVersion, bool> virtualNeedsRemove =
					ver => {
						foreach (var solve in ver.ResolvedDependancies)
							if (!removed.Contains(solve)) return false;
						return true;
					};

				toRemove = toRemove.Where(ver =>
					!ver.VirtualPackage ||
					virtualNeedsRemove(ver)).ToArray();

				BuildRemoveList(toRemove, removeOrder, removed);
			}
		}
		static PackageVersion[] BuildRemoveList(IEnumerable<PackageVersionFilter> packageFilters)
		{
			var removeItems = new List<PackageVersion>();
			foreach (var filter in packageFilters)
			{
				var removeVersions = filter.Filter(
					appDB.PackageVersions.Where(version => version.Installed));


				BuildRemoveList(removeVersions, removeItems, new HashSet<PackageVersion>());
			}

			removeItems.Reverse();
			return removeItems.ToArray();
		}
		static void Remove(PackageVersion version)
		{
			if (!version.Installed) return;

			CliPackageManager.WriteDebug("removing {0}...", version);

			if (!version.VirtualPackage)
			{
				if (!version.Cached) Download(version.VersionString);

				var info = new FileInfo(version.LocalFileName);

				info.PerformAction("silent remove");

			}
			version.Installed = false;
			version.Dependancy = false;

			appDB.SubmitChanges();
		}
		static void Remove(IEnumerable<PackageVersion> removeList)
		{
			foreach (var version in removeList)
				Download(version.VersionString);

			foreach (var version in removeList)
				Remove(version);
		}
		/// <summary>
		/// Removes selected and dependant packages.
		/// Approves removing after building remove order.
		/// </summary>
		/// <param name="versionFilters">List of filters describing initial packages to remove.</param>
		public static void Remove(IEnumerable<PackageVersionFilter> versionFilters)
		{
			CliPackageManager.WriteDebug("building remove list...");
			var list = BuildRemoveList(versionFilters);
			CliPackageManager.WriteDebug("OK\n");

			if (!RemoveApprover.Approve(typeof(PackageManager), list)) return;

			Remove(list);
		}
		#endregion Remove functions

		/// <summary>
		/// Downloads specified package version to cache.
		/// </summary>
		/// <param name="versionStringValue">Version to download.</param>
		/// <remarks>Doesn't edit database.</remarks>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string")]
		public static void Download(string versionStringValue)
		{
			var version = appDB.PackageVersions
				.Where(ver => ver.VersionString == versionStringValue)
				.FirstOrDefault();

			if (version == null) throw new KeyNotFoundException(versionStringValue);

			if ((version.LocalFileName != null) && File.Exists(version.LocalFileName)) return;

			CliPackageManager.WriteDebug("downloading {0}...", version);
			using (var client = new WebClient())
				client.DownloadFile(new Uri(version.Uri), version.LocalFileName);
			CliPackageManager.WriteDebug("OK\n");
		}

		/// <summary>
		/// Detects if built-in package version is suitable.
		/// </summary>
		/// <param name="version">Version to check</param>
		static void HandleBuiltInInternal(PackageVersion version)
		{
			Debug.Assert(version.SourceID == "built-in");

			var osver = Environment.OSVersion.Version;

			#region WindowsNT
			if (version.VersionString.StartsWith("WindowsNT", StringComparison.Ordinal))
			{
				if (Environment.OSVersion.Platform != PlatformID.Win32NT)
				{
					version.Installed = false;
					return;
				}

				var verStrings = version.VersionString.Substring("WindowsNT ".Length).Split('.');
				
				if (verStrings.Length == 1)
				{
					int major = int.Parse(verStrings[0], CultureInfo.InvariantCulture);			
					version.Installed = major == osver.Major;
					return;	
				}

				if (verStrings.Length == 2)
				{
					int major = int.Parse(verStrings[0], CultureInfo.InvariantCulture);
					int minor = int.Parse(verStrings[1], CultureInfo.InvariantCulture);
					version.Installed = (major == osver.Major) && (minor == osver.Minor);
					return;
				}

				if (verStrings.Length == 3)
				{
					int major = int.Parse(verStrings[0], CultureInfo.InvariantCulture);
					int minor = int.Parse(verStrings[1], CultureInfo.InvariantCulture);
					int build = int.Parse(verStrings[2], CultureInfo.InvariantCulture);
					version.Installed = (major == osver.Major) && (minor == osver.Minor) && (build == osver.Build);
					return;
				}

				version.Installed = false;
			}
			#endregion WindowsNT

			#region AMD64
			if (version.VersionString == "AMD64")
			{
				version.Installed = IntPtr.Size == 8;
				return;
			}
			#endregion
		}
		static void HandleBuiltIn(PackageVersion version)
		{
			HandleBuiltInInternal(version);
			version.Suitable = version.Installed;
		}
		/// <summary>
		/// Rebuilds suitable list.
		/// </summary>
		public static void RebuildSuitableVersionList()
		{
			CliPackageManager.WriteDebug("rebuilding suitable version list...");
				foreach (var version in appDB.PackageVersions)
					version.Suitable = false;

			appDB.SubmitChanges();

			int added = 0;
			do
			{
				added = 0;
				foreach (var version in appDB.PackageVersions.Where(ver => !ver.Suitable))
				{
					Debug.Assert(!version.Suitable);
					if (version.VirtualPackage)
					{
						if (version.IsBuiltIn)
						{
							HandleBuiltIn(version);
							if (version.Suitable) added++;
							continue;
						}
						version.Suitable = version.VersionDependancies.Where(dependancy =>
							dependancy.Resolvers.Where(resolver => resolver.Suitable).Count() > 0).Count() > 0;
						if (version.Suitable) added++;
					} else
					{
						bool notSuitable = false;
						foreach (var dependancy in version.VersionDependancies)
							if (dependancy.Resolvers.Where(resolver => resolver.Suitable).Count() == 0)
							{
								notSuitable = true;
								break;
							}
						version.Suitable = !notSuitable;
						if (version.Suitable) added++;
					}
				}
				appDB.SubmitChanges();
			} while (added > 0);

			CliPackageManager.WriteDebug("OK\n");
			
			bool os = false;
			var winnt = appDB.Packages.Single(package => package.PackageName == "WindowsNT").InstalledVersion;
			if (winnt != null)
			{
				os = true;
				CliPackageManager.WriteDebug("{0} detected\n", winnt.VersionString);
			}

			if (!os)
				CliPackageManager.WriteDebug("warning! os version is not detected\nprogram may not work correctly\n");
		}

		#region Installation functions
		static void Install(PackageVersion version)
		{
			CliPackageManager.WriteDebug("installing {0}...", version);
			if (!version.VirtualPackage)
			{
				var info = new FileInfo(version.LocalFileName);

				info.PerformAction("silent install");
			}
			version.Installed = true;

			appDB.SubmitChanges();
		}
		static PackageVersion[] BuildInstallList(IEnumerable<PackageVersionFilter> packageFilters)
		{
			// TODO: consider using ToLookup
			var dependancyTree = new VersionDependencyTree();
			var roots = new List<PackageVersion>();
			foreach (var filter in packageFilters)
				roots.Add(filter.BuildDependancyTree(appDB, filter, dependancyTree));

			return dependancyTree.TopologicalSort();
		}
		static void Install(IEnumerable<PackageVersion> installOrder)
		{
			foreach (var installItem in installOrder)
				if (!installItem.Cached && !installItem.VirtualPackage)
					Download(installItem.VersionString);

			foreach (var installItem in installOrder)
				Install(installItem);
		}

		/// <summary>
		/// Approver for installations.
		/// </summary>
		public static event EventHandler<ApproveEventArgs<IEnumerable<PackageVersion>>> InstallApprover;
		/// <summary>
		/// Approver for removing.
		/// </summary>
		public static event EventHandler<ApproveEventArgs<IEnumerable<PackageVersion>>> RemoveApprover;
		/// <summary>
		/// Installs selected packages and their dependancies.
		/// Approves installation after building install list.
		/// </summary>
		/// <param name="packageFilters">List of filters describing initial packages to install.</param>
		public static void Install(IEnumerable<PackageVersionFilter> packageFilters)
		{
			CliPackageManager.WriteDebug("building install list...");
			var installOrder = BuildInstallList(packageFilters);
			CliPackageManager.WriteDebug("OK\n");

			if (!InstallApprover.Approve(typeof(PackageManager), installOrder))
				return;

			Install(installOrder);
		}
		#endregion Installation functions

		#region Configuration
		/// <summary>
		/// Removes source by its name.
		/// </summary>
		/// <param name="sourceName">Name of source to delete.</param>
		public static void RemoveSource(string sourceName)
		{
			if (sourceName == "built-in") throw new InvalidOperationException("can't remove built-in source");
			CliPackageManager.WriteDebug("removing source {0}...", sourceName);
			//using (var transaction = new TransactionScope())
			//{
				var source = appDB.PackageSources
					.Where(src => src.Name == sourceName)
					.FirstOrDefault();

				if (source == null) throw new KeyNotFoundException(sourceName);

				appDB.PackageVersions.DeleteAllOnSubmit(source.PackageVersions);
				appDB.PackageSources.DeleteOnSubmit(source);

				appDB.SubmitChanges();

				RemoveEmptyPackages();

			//    transaction.Complete();
			//}
			CliPackageManager.WriteDebug("OK\n");
		}

		/// <summary>
		/// Adds source to database.
		/// </summary>
		/// <param name="sourceName">Name for source.</param>
		/// <param name="uri">Source uri.</param>
		public static void AddSource(string sourceName, Uri uri)
		{
			var source = new PackageSource() {
				Name = sourceName,
				Uri = uri.ToString(),
				Version = PackageSource.Never,
			};

			appDB.PackageSources.InsertOnSubmit(source);
			appDB.SubmitChanges();
		}
		/// <summary>
		/// Adds source to database.
		/// </summary>
		/// <param name="sourceName">Name for source.</param>
		/// <param name="uri">Source uri.</param>
		public static void AddSource(string sourceName, string uri)
		{
			var source = new PackageSource() {
				Name = sourceName,
				Uri = uri,
				Version = PackageSource.Never,
			};

			appDB.PackageSources.InsertOnSubmit(source);
			appDB.SubmitChanges();
		}
		#endregion Configuration

		#region Utils
		static void RemoveEmptyPackages()
		{
			appDB.Packages.DeleteAllOnSubmit(
				appDB.Packages
				.Where(package => package.PackageVersions.Count == 0));

			appDB.SubmitChanges();
		}
		#endregion Utils
	}
}
